#include <dirent.h>
#include <fcntl.h>
#include <paths.h>
#include <stdlib.h>
#include <sys/wait.h>

#include "soh.h"
#include "unix.h"

/**********************************************************************
This file contains functions for running Unix programs. One of the 
paradigms of Unix philosophy sounds like this: "a program should do 
one thing and do it well". This program tries to follow this philosophy,
so the main task is to process HTTP requests from the client, and the
rest of the work is delegated to other programs. For example, to send a
file to a client, all you need to do is send it the correct HTTP headers
and call the "cat" program, replacing stdout with the client's socket
descriptor.
***********************************************************************/



/**************************
*          UTILS          *
**************************/

int createPipe( int* pipeInput, int* pipeOutput ){
	
	int i;
	int pipes[2];
	
	//create pipe
	if( pipe(pipes) ){
		perror( "create pipes" );
		return -1;
	}
	
	//set close_on_exec flag on pipe
	for( i = 0; i < 2; i++ ) if( fcntl(pipes[i], F_SETFD, FD_CLOEXEC) ){
		perror( "set close_on_exec flag on pipe" );
		for( i = 0; i < 2; i++ ) close( pipes[i] );
		return -1;
	}
	
	*pipeInput  = pipes[1]; 
	*pipeOutput = pipes[0];
	
	return 0;
}

static struct progdesc* getUnixProgramByAlias( struct instance* this, const char* alias ){
	
	int i;
	
	for( i = 0; this->progs[i].name; i++ ){
		if( !this->progs[i].path ) continue;
		if( strcmp(alias, this->progs[i].alias) ) continue;
		return &this->progs[i];
	}
	
	return NULL;
}

/********************************
*          RUN PROGRAM          *
********************************/

int unixProgramResult( pid_t programPid, const char* programName ){
	
	int wstatus;
	pid_t waitres;
	
	//wait while programs works
	waitres = waitpid( programPid, &wstatus, 0 );
	if( waitres < 0 ) {
		perror( "waitpid" );
		return -2;
	}
	
	//print result
	if( WIFSIGNALED(wstatus) ) {
		printf( "%s was killed by signal %d\n", programName, WTERMSIG(wstatus) );
		return -1;
	}
	
	if( WIFEXITED(wstatus) ){
		if( WEXITSTATUS(wstatus) ){
			printf( "%s return code %d\n", programName, WEXITSTATUS(wstatus) );
			return -1;
		}
		return 0;
	} 
	
	printf( "FATAL: waitpid return unknown status.\n" );
	return -2;
}

pid_t startUnixProgram( char* argv[], char* envp[], int stdio[3] ){
	
	int i;
	int res;
	pid_t forkRes;
	FILE* stdFiles[] = { stdin, stdout, stderr };
	char* errstr[] = { "dup2 stdin", "dup2 stdout", "dup2 stderr" };
	
	//after fork main thread return 
	//and child be works in this function
	forkRes = fork();
	if( forkRes ){
		if( forkRes < 0 ) perror( "startUnixProgram fork" );
		return forkRes;
	}
	
	for( i = 0; i < 3; i++ ) if( stdio[i] >= 0 ){
		res = dup2( stdio[i], fileno(stdFiles[i]) );
		if( res < 0 ){
			perror( errstr[i] );
			exit( 1 );
		}
	}
	
	execve( argv[0], argv, envp );
	perror( "execve" );
	exit( 66 );
}

static int startUnixPrograms( struct instance* this, char** argv[], int count ){
	
	int i;
	int res;
	int pipeInput;
	int pipeOutput;
	pid_t pids[count];
	int stdio[3] = { -1, -1, -1 };
	int programResult;
	
	//init some vars
	res = 0;
	pipeInput = -1;
	pipeOutput = -1;
	
	/*********   start programs   *********/
	for( i = 0; i < count; i++ ){
		
		//if last, then connect prorgamm out to client socket
		if( i + 1 == count ) stdio[1] = this->clientSocket;
		else{
			//create new pipe
			res = createPipe( &pipeInput, &pipeOutput );
			if( res ){
				if( pipeOutput >= 0 ) close( pipeOutput );
				goto l_monitor;
			}
			
			//connect programm stdout to pipe input
			stdio[1] = pipeInput;
		}
		
		//start program
		pids[i] = startUnixProgram( argv[i], this->envp, stdio );
		if( pids[i] < 0 ){
			if( pipeInput >= 0  ) close( pipeInput  );
			if( pipeOutput >= 0 ) close( pipeOutput );
			goto l_monitor;
		}
		
		//close our copy pipeInput of current pipe
		if( pipeInput >= 0 ){
			close( pipeInput );
			pipeInput = -1;
		}
		
		//close our copy pipeOutput of previous pipe
		if( stdio[0] >= 0 ) close( stdio[0] );
		
		//connect current pipeOutput to stdin of next programm
		stdio[0] = pipeOutput;
	}
	
	/*********   wait results   *********/
l_monitor:
	//in case if error occurred while starting programs
	if( i != count ){
		count = i;
		printf( "Start unix programs failed.\n" );
	}
	
	//wait results
	for( i = 0; i < count; i++ ){
		programResult = unixProgramResult( pids[i], argv[i][0] );
		if( programResult == -1 ) res = -1;
		else if( programResult == -2 ) return -2;
	}
	
	return res;
}

/********************************************
*          SEARCH PROGRAMS ON HOST          *
********************************************/

static int searchUnixProgramInDir( struct progdesc* prog, DIR* dir, char* path ){
	
	int res;
	char tmpbuf[8192];
	struct dirent* dirent;
	struct stat statbuf;
	
	for( dirent = readdir(dir); dirent; dirent = readdir(dir) ){
		
		//compare name
		if( strcmp(prog->name, dirent->d_name) ) continue;
		
		//full path to file
		snprintf( tmpbuf, sizeof(tmpbuf), "%s/%s", path, dirent->d_name );
		
		//get file info via stat and check type
		res = stat( tmpbuf, &statbuf );
		if( res || (statbuf.st_mode & S_IFMT) != S_IFREG ) continue;
		
		//check file is executable
		if( !(statbuf.st_mode & S_IXUSR) & !(statbuf.st_mode & S_IXGRP)  & !(statbuf.st_mode & S_IXOTH) ) continue;
		
		//set programm path
		prog->path = customMalloc( strlen(tmpbuf) + 1 );
		if( !prog->path ) return -2;
		sprintf( prog->path, "%s", tmpbuf );
		
		//search next program
		return 1;
		
	}
	
	return 0;
}


#define PARSE_MAX_PATHS 20
int searchUnixPrograms( struct instance* this ){
	
	int i;
	int i2;
	int res;
	char* ePath;
	size_t ePathOffset;
	int pathsCount;
	char paths[PARSE_MAX_PATHS][256];
	
	DIR* dir;
	
	printf( "Searching for avaliable utils... " );
	fflush( stdout );
	
	//init some vars
	pathsCount = 0;
	ePathOffset = 0;
	
	//get env PATH
	ePath = getenv( "PATH" );
	if( !ePath ) ePath = _PATH_STDPATH;
	
	/*********   parse ePath into paths   *********/
	for( pathsCount = 0; pathsCount < PARSE_MAX_PATHS; pathsCount++ ){
		
		res = parserCore( ePath, ':', paths[pathsCount], sizeof(paths[pathsCount]), &ePathOffset );
		if( res < 0 ){
			printf( "%s: Failed to parse PATH environment variable.\n", __FUNCTION__ );
			return -2; //error
		}
		
		if( !res ) break;
	}
	
	/*********   search progs   *********/
	
	this->progs = unixProgramDB;
	
	//i  - progs DB
	for( i = 0; unixProgramDB[i].name; i++ ){
		
		//i2 - paths
		for( i2 = 0; i2 < pathsCount; i2++ ){
			
			dir = opendir( paths[i2] );
			if( !dir ) continue;
			
			res = searchUnixProgramInDir( &unixProgramDB[i], dir, paths[i2] );
			
			closedir( dir );
			
			if( res < 0 ) return -2;
			if( res > 0 ) break;
		}
		
	}
	
	printf( "done.\n" );
	
	return 0;
}


/*************************************
*          DOWNLOAD HANDLER          *
*************************************/

static int checkIndexValidity( struct file_db* indexItem ){
	
	int res;
	char type;
	struct stat statbuf;
	
	//get info about file
	res = lstat( indexItem->desc + 1, &statbuf );
	if( res ){
		perror( "archiver stat" );
		return -2;
	}
	
	//convert stat type to our type
	res = convertStatType( &statbuf, &type );
	if( res ) return -2;
	
	//make sure the type in the index matches the type of the file on disk
	if( (indexItem->desc[0] & FILEDESC_TYPE_MASK) != type ){
		printf( "FATAL: File type in the index does not match with file type on the disk.\n" );
		return -2;
	}
	
	//for file also check size
	if( (indexItem->desc[0] & FILEDESC_TYPE_MASK) != FILEDESC_TYPE_DIR ){
		if( indexItem->size != statbuf.st_size ){
			printf( "FATAL: Filesize in the index does not match with filesize on the disk.\n" );
			return -2;
		}
	}
	
	return 0;
}

static int getFileMime( struct instance* this, char* filename, char* mimeBuf, int mimeBufLen ){
	
	int res;
	int offset;
	int pipeInput;
	int pipeOutput;
	int stdio[3];
	struct progdesc* file;
	char* args[6];
	pid_t filePid;
	
	//init some vars
	offset = 0;
	
	//get file util
	file = getUnixProgramByAlias( this, "file" );
	if( !file ) return -1;
	
	//prepare args
	args[0] = file->path;
	args[1] = "-i";
	args[2] = "-b";
	args[3] = "--";
	args[4] = filename;
	args[5] = NULL;
	
	//create pipe
	res = createPipe( &pipeInput, &pipeOutput );
	if( res ) return -1;
	
	//route output to pipe
	stdio[0] = -1;        //default stdin
	stdio[1] = pipeInput; //stdout to pipe input
	stdio[2] = -1;        //default stderr
	
	//start file
	filePid = startUnixProgram( args, this->envp, stdio );
	close( pipeInput );
	if( filePid < 0 ){
		close( pipeOutput );
		return -1;
	}
	
	//read mime from pipe
l_readNext:
	res = read( pipeOutput, mimeBuf + offset, mimeBufLen - offset );
	if( res > 0 ){
		offset += res;
		goto l_readNext;
	}
	
	//set null-terminator and close pipe
	mimeBuf[offset-1] = 0x00;
	close( pipeOutput );
	
	return unixProgramResult( filePid, args[0] );
}

int unixDownloadHandler( struct instance* this, struct file_db* requested, char* format ){
	
	int res;
	char buf[4096];
	char encodedFilename[2048];
	int  headersLength;
	char parsedFormat[8];
	size_t formatOffset;
	struct progdesc* reader;
	struct progdesc* compressor;
	int programsCount;
	char* readerArgv[6];
	char* compressorArgv[2];
	char** argvArr[3];
	char fileExtension[16];
	char flagIsRaw;
	char flagIfView;
	char mime[1024];
	
	//init some vars
	formatOffset = 0;
	reader = NULL;
	compressor = NULL;
	fileExtension[0] = 0x00;
	flagIsRaw = 0x00;
	flagIfView = 0x00;
	
	//check the validity of the index
	res = checkIndexValidity( requested );
	if( res ){
		httpSendStatus( this, 500, "Internal server error." );
		return res;
	}
	
	/*********   select reader   *********/
	
	//if view in browser requested
	if( !strcmp("view", format) ){
		flagIfView = 0xff;
		goto l_raw;
	}
	
	//if raw format requested
	if( !format[0] ){
l_raw:
		//raw format is available only for regular files, remember?
		if( (requested->desc[0] & FILEDESC_TYPE_MASK) != FILEDESC_TYPE_FILE ) goto l_501;
		reader = getUnixProgramByAlias( this, "raw" );
		if( !reader ) goto l_501;
		flagIsRaw = 0xff;
		goto l_prepare_argv;
	}
	
	//parse format
	res = parserCore( format, '.', parsedFormat, sizeof(parsedFormat), &formatOffset );
	if( res < 0 ) goto l_400;
	
	//search suitable reader
	reader = getUnixProgramByAlias( this, parsedFormat );
	if( !reader ) goto l_501;
	
	if( reader->type != PROGDESC_TYPE_ARCHIVER ){
		
		//not a regular file can be sent only with the help of archivers, remember?
		if( (requested->desc[0] & FILEDESC_TYPE_MASK) != FILEDESC_TYPE_FILE ) goto l_501;
		
		if( reader->type != PROGDESC_TYPE_COMPRESSOR ) goto l_501;
		compressor = reader;
		
		reader = getUnixProgramByAlias( this, "raw" );
		if( !reader ) goto l_501;
	}
	
	/*********   select compressor   *********/
	
	//parse next part of format and search compressor if need
	if( res ){
		
		//e.g. xz.xz
		if( compressor ) goto l_400;
		
		res = parserCore( format, '.', parsedFormat, sizeof(parsedFormat), &formatOffset );
		if( res < 0 ) goto l_400;
		if( res ) goto l_501;// e.g. tar.xz.*
		
		compressor = getUnixProgramByAlias( this, parsedFormat );
		if( !compressor ) goto l_501;
		if( compressor->type != PROGDESC_TYPE_COMPRESSOR ) goto l_501;
	}
	
	/*********   file extension   *********/
	
	if( reader->type == PROGDESC_TYPE_ARCHIVER ){
		if( compressor ){
			res = sprintf( fileExtension, ".%s.%s", reader->alias, compressor->alias );
		} else {
			res = sprintf( fileExtension, ".%s", reader->alias );
		}
	} else {
		res = sprintf( fileExtension, ".%s", compressor->alias );
	}
	
	if( res < 0 ) goto l_500;
	
	/*********   prepare argv for programs   *********/
	
l_prepare_argv:
	
	printf( "using reader %s\n", reader->name );
	if( compressor ) printf( "using compressor %s\n", compressor->name );
	programsCount = 1;
	
	//reader argv
	readerArgv[0] = reader->path;
	if( !strcmp(reader->name, "tar") ){
		
		readerArgv[1] = "-cf";
		readerArgv[2] = "-"; 
		readerArgv[3] = "--"; 
		readerArgv[4] = requested->desc + 1;
		readerArgv[5] = NULL;
		
	}else{
		
		readerArgv[1] = "--"; 
		readerArgv[2] = requested->desc + 1;
		readerArgv[3] = NULL;
		
	}
	
	argvArr[0] = readerArgv;
	
	//compressor argv
	if( !compressor ) argvArr[1] = NULL;
	else{
		
		compressorArgv[0] = compressor->path;
		compressorArgv[1] = NULL;
		
		argvArr[1] = compressorArgv;
		argvArr[2] = NULL;
		
		programsCount = 2;
		
	}
	
	/*********   send HTTP headers   *********/
	
	//encode filename
	res = uriEncode( requested->desc + 1, encodedFilename, sizeof(encodedFilename) );
	if( res < 0 ) goto l_500;
	
	//if a raw file is requested, then we can send its size
	if( flagIsRaw ){
		if( flagIfView ){
			
			res = getFileMime( this, requested->desc + 1, mime, 1024 );
			if( res == -1 ) goto l_500;
			if( res == -2 ){
				httpSendStatus( this, 500, "Internal server error." );
				return -2;
			}
			
			res = sprintf( buf,
				"%s 200 OK\r\n"           //this->protocol
				"Content-Type: %s\r\n"    //mime
				"Content-Length: %li\r\n" //requested->size
				"\r\n",
				this->protocol, mime, requested->size
			);
		} else {
			res = sprintf( buf,
				"%s 200 OK\r\n"                                        //this->protocol
				"Content-Disposition: attachment; filename=\"%s\"\r\n" //encodedFilename
				"Content-Length: %li\r\n"                              //requested->size
				"\r\n",
				this->protocol, encodedFilename, requested->size
			);
		}
		
		

	//if an archiver and/or compressor is used, you can't know the size of the output file
	} else {
		res = sprintf( buf,
			"%s 200 OK\r\n"                                          //this->protocol
			"Content-Disposition: attachment; filename=\"%s%s\"\r\n" //name.extension
			"\r\n",
			this->protocol, encodedFilename, fileExtension 
		);
	}
	
	if( res < 0 ) goto l_500;
	headersLength = res;
	
	//send headers
	res = sendData( this->clientSocket, buf, headersLength );
	if( res ) return -1;
	
	/*********   start unix programs   *********/
	
	printf( "<<< Uploading %s...\n", requested->desc + 1 );
	res = startUnixPrograms( this, argvArr, programsCount );
	if( !res ) printf( "Done.\n" );
	else printf( "Failed.\n" );
	return res;
	
l_400: return httpSendStatus( this, 400, "Bad request." );
l_500: return httpSendStatus( this, 500, "Internal server error." );
l_501: return httpSendStatus( this, 501, "Not implemented." );
}
